﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Data;
using System.Text.RegularExpressions;
using WindowsPhoneMvp.Util;

namespace WindowsPhoneMvp.Events
{
    internal class EventToPresenter
    {
        static readonly Regex bindingParser = new Regex(@"^\{Binding[ ]?[Path=]*?(?<path>[\w\.]+)*\}$", RegexOptions.Compiled);
        static readonly Type[] delegateCache = new[] { typeof(Action), typeof(Action<,>), typeof(Action<,,>), typeof(Action<,,,>) };
        readonly WeakReference presenter;

        public EventToPresenter(IPresenter presenter)
        {
            this.presenter = new WeakReference(presenter);
        }

        public void Invoke(FrameworkElement element, object source, EventDataItem eventData, object eventArgs)
        {
            var parameters = new List<object>();
            if(eventData.Parameters != null && eventData.Parameters.Any())
            {
                var sourceType = source.GetType();
                foreach(var param in eventData.Parameters)
                {
                    if(param.Equals("$sender$"))
                    {
                        parameters.Add(source);
                    }
                    else if(param.Equals("$args$"))
                    {
                        parameters.Add(eventArgs);
                    }
                    else if(param.StartsWith("{"))
                    {
                        var results = bindingParser.Matches(param);
                        if(results.Count == 0)
                            throw new ArgumentException(string.Format("'{0}' couldn't be parsed as binding syntax", param));
                        var b = new Binding(results[0].Groups["path"].Value) { Source = element.DataContext };
                        var be = new BindingEvaluator(b);
                        parameters.Add(be.Value);
                    }
                    else
                    {
                        var property = sourceType.GetProperty(param);
                        var value = property.GetValue(source, null);
                        parameters.Add(value);
                    }
                }
            }

            object presenterObj;
            if(presenter.IsAlive)
                presenterObj = presenter.Target;
            else
                throw new ObjectDisposedException("Presenter has already been disposed.");

            if(eventData.CachedDelegate == null)
            { //Create a cached invoke delegate if an Action delegate matches
                var presenterType = presenterObj.GetType();

                MethodInfo targetMethod;
                if (parameters.Any(x => x == null) == false)
                {
                    targetMethod = presenterType.GetMethod(eventData.TargetMethod, parameters.Select(x => x.GetType()).ToArray());
                }
                else
                { //could not be found by parameter types
                    targetMethod = presenterType.GetMethod(eventData.TargetMethod, BindingFlags.Public | BindingFlags.Instance);
                }

                if(targetMethod == null)
                    throw new MissingMethodException(string.Format("Method {0} does not exist on {1}", eventData.TargetMethod, presenterType.Name));
                var @delegate = delegateCache.FirstOrDefault(x => x.GetGenericArguments().Count() == parameters.Count);

                if(@delegate != null)
                {
                    var typedDelegate = parameters.Count > 0
                                            ? @delegate.MakeGenericType(parameters.Select(x => x.GetType()).ToArray())
                                            : @delegate;
                    eventData.CachedDelegate = Delegate.CreateDelegate(typedDelegate, presenterObj, targetMethod, true);
                }
                else
                { //reflection invoke
                    try
                    {
                        targetMethod.Invoke(presenterObj, parameters.ToArray());
                    }
                    catch(TargetInvocationException ex)
                    { // bubble real error
                        throw ex.InnerException ?? ex;
                    }
                }
            }
            if(eventData.CachedDelegate != null)
            { //delegate invoke
                try
                {
                    eventData.CachedDelegate.DynamicInvoke(parameters.ToArray());
                }
                catch(TargetInvocationException ex)
                { // bubble real error
                    throw ex.InnerException ?? ex;
                }
            }
        }
    }
}
